<!--
 * @Date: 2025-03-04 09:00:14
 * @LastEditors: DOIT 109971423+DOIT008@users.noreply.github.com
 * @LastEditTime: 2025-06-19 16:43:21
 * @Author: DOIT 109971423+DOIT008@users.noreply.github.com
 * @Description: 支持定高/不定高的虚拟滚动列表
-->
<!--
* 不定高实现方案：预估高度+实际测量+动态调整
* 1. 使用estimatedHeight作为初始预估高度
* 2. 实际渲染后测量每个项的offsetHeight并缓存
* 3. 滚动时根据缓存的高度计算可见区域
* 4. 添加前后缓冲区避免快速滚动出现空白
-->

<template>
  <div class="virtual-list" @scroll="handleScroll" :style="{ height: `${props.boxHeight}px` }">
    <!-- 滚动占位容器 - 用于支撑滚动条高度 -->
    <div class="virtual-list-box" :style="{ height: totalHeight + 'px' }"></div>
    <!-- 可视区域内的元素列表 - 使用transform定位 -->
    <div class="visual-box" :style="{ transform: `translateY(${offsetTop}px)` }">
      <div 
        v-for="index in visibleIndices" 
        :key="index" 
        :ref="el => setItemRef(index, el)"
        class="virtual-item"
      >
        <slot name="default" :item="props.data[index]" :index="index"></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, computed, defineProps, nextTick } from 'vue';
const props = defineProps({
  // 巨量源数据
  data: {
    type: Array,
    default: () => [],
    required: true
  },
  // 预估高度，用于不定高模式（定高模式下也作为元素高度）
  estimatedHeight: {
    type: [Number, String],
    default: 50
  },
  // 是否启用不定高模式
  useDynamicHeight: {
    type: Boolean,
    default: true
  },
  // 盒子高度
  boxHeight: {
    type: [Number, String],
    default: 0,
    required: true
  },
  // 缓冲区大小（前后各n项）
  bufferSize: {
    type: Number,
    default: 5
  }
})
// 状态管理
const startIndex = ref(0) // 可视区域起始索引
const endIndex = ref(1) // 可视区域结束索引
const heightMap = ref({}) // 存储每个项的实际高度 { index: height }
const totalHeight = ref(0) // 列表总高度

// 计算可视项索引数组(从startIndex到endIndex的连续索引数组)
const visibleIndices = computed(() => {
  return Array.from({ length: Math.max(0, endIndex.value - startIndex.value) }, (_, i) => startIndex.value + i);
})

/**
 * 计算可视区域的偏移量（距离顶部的距离）
 * @returns {Number} 偏移像素值
 * @description 用于通过transform: translateY()定位可视区域内容
 */
const offsetTop = computed(() => {
  // 定高模式直接计算偏移量
  if (!props.useDynamicHeight) {
    return startIndex.value * Number(props.estimatedHeight);
  }
  // 不定高模式：累加起始索引前所有项的高度
  return props.data.slice(0, startIndex.value).reduce((sum, _, index) => {
    return sum + (heightMap.value[index] || Number(props.estimatedHeight));
  }, 0);
})

/**
 * 更新列表总高度
 * @description 根据缓存的高度数据重新计算整个列表的总高度
 * 用于设置占位容器高度，确保滚动条正常工作
 */
const updateTotalHeight = () => {
  totalHeight.value = props.data.reduce((sum, _, index) => {
    return sum + (heightMap.value[index] || Number(props.estimatedHeight));
  }, 0);
}

/**
 * 设置列表项ref并测量实际高度
 * @param {Number} index - 列表项索引
 * @param {HTMLElement} el - DOM元素（通过ref传递）
 * @description 组件渲染后测量实际高度并缓存，高度变化时更新缓存并重新计算
 */
const setItemRef = (index, el) => {
  if (el) {
    nextTick(() => {
      const height = el.offsetHeight;
      // 高度变化时才更新缓存并重新计算
      if (heightMap.value[index] !== height) {
        heightMap.value[index] = height;
        updateTotalHeight();
        handleScroll(); // 高度变化后重新计算可见区域
      }
    });
  }
}

/**
 * 根据滚动位置查找起始索引
 * @param {Number} scrollTop - 滚动条距离顶部的距离
 * @returns {Number} 可视区域的起始索引
 * @description 通过累加高度找到第一个可见项的索引
 */
const findStartIndex = (scrollTop) => {
  if (scrollTop === 0) return 0;
  let sum = 0;
  for (let i = 0; i < props.data.length; i++) {
    const height = heightMap.value[i] || Number(props.estimatedHeight);
    sum += height;
    if (sum > scrollTop) {
      return i;
    }
  }
  return props.data.length - 1;
}

/**
 * 初始化：组件挂载后执行
 * @description 初始化时计算一次可视区域
 */
onMounted(() => {
  handleScroll()
})

/**
 * 滚动事件处理函数
 * @description 根据滚动位置动态计算可视区域的起始和结束索引
 * 是虚拟滚动的核心逻辑实现
 */
const handleScroll = () => {
  // 获取容器元素
  const container = document.getElementsByClassName('virtual-list')[0];
  if (!container) return;

  const containerHeight = container.clientHeight; // 容器可视高度
  const scrollTop = container.scrollTop; // 滚动条位置

  if (props.useDynamicHeight) {
    // ===== 不定高模式处理 =====
    // 1. 查找起始索引
    startIndex.value = findStartIndex(scrollTop);

    // 2. 计算结束索引：累加高度直到超过容器高度
    let accumulatedHeight = 0;
    endIndex.value = startIndex.value;
    while (endIndex.value < props.data.length) {
      const itemHeight = heightMap.value[endIndex.value] || Number(props.estimatedHeight);
      accumulatedHeight += itemHeight;

      // 当累加高度超过容器高度时停止
      if (accumulatedHeight > containerHeight) break;
      endIndex.value++;
    }

    // 3. 添加前后缓冲区（防止快速滚动出现空白）
    startIndex.value = Math.max(0, startIndex.value - props.bufferSize);
    endIndex.value = Math.min(props.data.length, endIndex.value + props.bufferSize);
  } else {
    // ===== 定高模式处理 =====
    const itemHeight = Number(props.estimatedHeight);
    startIndex.value = Math.floor(scrollTop / itemHeight);
    endIndex.value = startIndex.value + Math.ceil(containerHeight / itemHeight);
  }
};
</script>

<style lang="scss" scoped>
.virtual-list {
  width: 100%;
  overflow: auto;
  position: relative;

  .virtual-list-box {
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
  }
}
</style>